package com.wlyuan.index.domain.source.repository;

import com.wlyuan.index.domain.source.Source;
import com.wlyuan.index.domain.source.valueobj.Modifiers;
import com.wlyuan.index.domain.source.valueobj.SourceId;
import com.wlyuan.index.infrastructure.config.SourceProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.var;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.redisson.api.map.event.EntryExpiredListener;
import org.redisson.api.map.event.EntryRemovedListener;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
@Slf4j
public class SourceRepositoryImpl implements SourceRepository {
    private final RedissonClient redissonClient;
    private final SourceProperties sourceProperties;
    private RMapCache<SourceId, Source> sourceMap;
    private RMapCache<SourceId, List<SourceId>> boundsMap;
    private static final String LOCK_NAME = "PC::LOCK::INDEX::SOURCE";

    private final EntryExpiredListener<SourceId, Source> expiredListener = event -> {
        if (logger.isDebugEnabled()) {
            logger.debug("Source expired: {} {}", event.getKey(), event.getValue());
        }
    };

    private final EntryRemovedListener<SourceId, Source> removedListener = event -> {
        if (logger.isDebugEnabled()) {
            logger.debug("Source removed: {} {}", event.getKey(), event.getValue());
        }
    };

    private int getMaxSize() {
        return sourceProperties.getMaxSize();
    }

    private int getExpireDays() {
        return sourceProperties.getExpireDays();
    }

    @PostConstruct
    public void initialize() {
        this.sourceMap = redissonClient.getMapCache("PC::CACHE::INDEX::SOURCE");
        // FIXME:  可以保存多个BOUNDS解决并发问题
        this.boundsMap = redissonClient.getMapCache("PC::CACHE::INDEX::BOUNDS");
        this.sourceMap.setMaxSize(this.getMaxSize());
        this.boundsMap.setMaxSize(this.getMaxSize());
        this.sourceMap.addListener(expiredListener);
        this.sourceMap.addListener(removedListener);
    }

    private boolean checkVersion(SourceId sourceId, long newVersion, long oldVersion) {
        if (oldVersion == newVersion) {
            if (logger.isDebugEnabled()) {
                logger.debug("[{}]版本已是最新: {}={}", sourceId, newVersion, oldVersion);
            }
            return false;
        }

        // 新数据版本<旧数据版本说明数据顺序可能不对
        if (newVersion < oldVersion) {
            logger.warn("[{}]版本发生变化: {}<{}", sourceId, newVersion, oldVersion);
            return false;
        }
        return true;
    }

    private boolean checkVersion(SourceId sourceId, Long newVersion, Long oldVersion) {
        if (null == oldVersion || null == newVersion) {
            return true;
        }
        return this.checkVersion(sourceId, (long) newVersion, (long) oldVersion);
    }

    private void addBounds(Source source) {
        var boundIds = this.boundsMap.get(source.getId());
        if (CollectionUtils.isEmpty(boundIds)) {
            return;
        }
        source.addBounds(boundIds);
    }

    @Override
    public Source ofId(SourceId sourceId) {
        var source = this.sourceMap.get(sourceId);
        if (null == source) {
            return null;
        }
        this.addBounds(source);
        return source;
    }

    @Override
    public SourceId save(Source source) {
        assert null != source;
        var sourceId = source.getId();
        var latest = this.sourceMap.get(sourceId);
        var expireDays = this.getExpireDays();
        if (null == latest) {
            this.sourceMap.fastPut(sourceId, source, expireDays, TimeUnit.DAYS);
            return sourceId;
        }

        if (this.checkVersion(sourceId, source.getVersion(), latest.getVersion())) {
            this.sourceMap.fastPut(sourceId, source, expireDays, TimeUnit.DAYS);
        }

        return sourceId;
    }

    @Override
    public void remove(SourceId sourceId) {
        this.sourceMap.remove(sourceId);
        var boundIds = this.boundsMap.get(sourceId);
        if (CollectionUtils.isNotEmpty(boundIds)) {
            boundIds.forEach(this.sourceMap::remove);
        }
        this.boundsMap.remove(sourceId);
    }

    @Override
    public List<Source> list(Collection<SourceId> sourceIds) {
        if (CollectionUtils.isEmpty(sourceIds)) {
            return Collections.emptyList();
        }
        var sources = this.sourceMap.getAll(new HashSet<>(sourceIds));
        return sourceIds.stream()
                .map(sources::get)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    @Override
    public Map<SourceId, Source> getSourceMap(Collection<SourceId> sourceIds) {
        if (CollectionUtils.isEmpty(sourceIds)) {
            return Collections.emptyMap();
        }
        return this.sourceMap.getAll(new HashSet<>(sourceIds));
    }

    @Override
    public void unbound(SourceId sourceId) {
        this.boundsMap.remove(sourceId);
    }

    @Override
    public void bound(SourceId sourceId, List<SourceId> boundIds) {
        assert null != sourceId;
        if (CollectionUtils.isEmpty(boundIds)) {
            return;
        }

        // FIXME: 存在并发问题，数据可能被覆盖
        // 保存源数据关系
        var latestBoundIds = this.boundsMap.get(sourceId);
        if (null == latestBoundIds) {
            this.boundsMap.fastPut(sourceId, boundIds);
        } else {
            var boundIdSet = new HashSet<>(latestBoundIds);
            if (!boundIdSet.containsAll(boundIds)) {
                boundIdSet.addAll(boundIds);
                this.boundsMap.fastPut(sourceId, new ArrayList<>(boundIdSet));
            }
        }
    }

    @Override
    public Map<SourceId, List<Source>> getBoundsMap(Collection<SourceId> sourceIds) {
        var boundIdsMap = this.boundsMap.getAll(new HashSet<>(sourceIds));
        if (MapUtils.isEmpty(boundIdsMap)) {
            return Collections.emptyMap();
        }

        var boundsMap = new HashMap<SourceId, List<Source>>();
        boundIdsMap.forEach((sourceId, boundIds) -> {
            // 查询boundIds对应的source列表
            boundsMap.put(sourceId, this.list(boundIds));
        });
        return boundsMap;
    }

    @Override
    public Map<SourceId, List<Source>> getBoundsMap(Collection<SourceId> sourceIds, Modifiers modifier) {
        var boundIdsMap = this.boundsMap.getAll(new HashSet<>(sourceIds));
        if (MapUtils.isEmpty(boundIdsMap)) {
            return Collections.emptyMap();
        }
        var boundsMap = new HashMap<SourceId, List<Source>>();
        boundIdsMap.forEach((sourceId, boundsIds) -> {
            var filtered = boundsIds.stream()
                    .filter(boundId -> boundId.getModifier() == modifier)
                    .collect(Collectors.toList());
            boundsMap.put(sourceId, this.list(filtered));
        });
        return boundsMap;
    }

    @Override
    public Collection<SourceId> listIds() {
        return new ArrayList<>(this.sourceMap.keySet());
    }
}
